package org.andengine.opengl.font;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.SparseArray;

import org.andengine.opengl.font.exception.FontException;
import org.andengine.opengl.texture.ITexture;
import org.andengine.opengl.texture.PixelFormat;
import org.andengine.opengl.util.GLState;
import org.andengine.util.adt.color.Color;
import org.andengine.util.adt.map.SparseArrayUtils;
import org.andengine.util.math.MathUtils;

import java.util.ArrayList;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 10:39:33 - 03.04.2010
 */
public class Font implements IFont {
    // ===========================================================
    // Constants
    // ===========================================================

    protected static final int LETTER_TEXTURE_PADDING = 1;

    // ===========================================================
    // Fields
    // ===========================================================
    private int mCurrentTextureX = Font.LETTER_TEXTURE_PADDING;
    private int mCurrentTextureY = Font.LETTER_TEXTURE_PADDING;
    protected final Paint mPaint;
    protected final FontMetrics mFontMetrics;
    protected final Canvas mCanvas = new Canvas();
    protected final Rect mTextBounds = new Rect();
    protected final float[] mTextWidthContainer = new float[1];
    private final FontManager mFontManager;
    private final ITexture mTexture;
    private final int mTextureWidth;
    private final int mTextureHeight;
    private final SparseArray<Letter> mManagedCharacterToLetterMap = new SparseArray<Letter>();
    private final ArrayList<Letter> mLettersPendingToBeDrawnToTexture = new ArrayList<Letter>();
    private final Paint mBackgroundPaint;
    private int mCurrentTextureYHeightMax;

    // ===========================================================
    // Constructors
    // ===========================================================

    public Font(final FontManager pFontManager, final ITexture pTexture, final Typeface pTypeface, final float pSize, final boolean pAntiAlias, final Color pColor) {
        this(pFontManager, pTexture, pTypeface, pSize, pAntiAlias, pColor.getARGBPackedInt());
    }

    public Font(final FontManager pFontManager, final ITexture pTexture, final Typeface pTypeface, final float pSize, final boolean pAntiAlias, final int pColorARGBPackedInt) {
        this.mFontManager = pFontManager;
        this.mTexture = pTexture;
        this.mTextureWidth = pTexture.getWidth();
        this.mTextureHeight = pTexture.getHeight();

        this.mBackgroundPaint = new Paint();
        this.mBackgroundPaint.setColor(Color.TRANSPARENT_ARGB_PACKED_INT);
        this.mBackgroundPaint.setStyle(Style.FILL);

        this.mPaint = new Paint();
        this.mPaint.setTypeface(pTypeface);
        this.mPaint.setColor(pColorARGBPackedInt);
        this.mPaint.setTextSize(pSize);
        this.mPaint.setAntiAlias(pAntiAlias);

        this.mFontMetrics = this.mPaint.getFontMetrics();
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    /**
     * @return the gap between the lines.
     */
    public float getLeading() {
        return this.mFontMetrics.leading;
    }

    /**
     * @return the distance from the baseline to the top, which is usually negative.
     */
    public float getAscent() {
        return this.mFontMetrics.ascent;
    }

    /**
     * @return the distance from the baseline to the bottom, which is usually positive.
     */
    public float getDescent() {
        return this.mFontMetrics.descent;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    public ITexture getTexture() {
        return this.mTexture;
    }

    @Override
    public void load() {
        this.mTexture.load();
        this.mFontManager.loadFont(this);
    }

    @Override
    public void unload() {
        this.mTexture.unload();
        this.mFontManager.unloadFont(this);
    }

    @Override
    public float getLineHeight() {
        return -this.getAscent() + this.getDescent();
    }

    @Override
    public synchronized Letter getLetter(final char pCharacter) throws FontException {
        Letter letter = this.mManagedCharacterToLetterMap.get(pCharacter);
        if (letter == null) {
            letter = this.createLetter(pCharacter);

            this.mLettersPendingToBeDrawnToTexture.add(letter);
            this.mManagedCharacterToLetterMap.put(pCharacter, letter);
        }
        return letter;
    }

    // ===========================================================
    // Methods
    // ===========================================================

    public synchronized void invalidateLetters() {
        final ArrayList<Letter> lettersPendingToBeDrawnToTexture = this.mLettersPendingToBeDrawnToTexture;
        final SparseArray<Letter> managedCharacterToLetterMap = this.mManagedCharacterToLetterMap;

		/* Make all letters redraw to the texture. */
        for (int i = managedCharacterToLetterMap.size() - 1; i >= 0; i--) {
            lettersPendingToBeDrawnToTexture.add(managedCharacterToLetterMap.valueAt(i));
        }
    }

    private float getLetterAdvance(final String pCharacterAsString) {
        this.mPaint.getTextWidths(pCharacterAsString, this.mTextWidthContainer);
        return this.mTextWidthContainer[0];
    }

    protected Bitmap getLetterBitmap(final Letter pLetter) throws FontException {
        final char character = pLetter.mCharacter;
        final String characterAsString = String.valueOf(character);

        final int width = pLetter.mWidth + (2 * Font.LETTER_TEXTURE_PADDING);
        final int height = pLetter.mHeight + (2 * Font.LETTER_TEXTURE_PADDING);

        final Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
        this.mCanvas.setBitmap(bitmap);

		/* Make background transparent. */
        this.mCanvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), this.mBackgroundPaint);

		/* Actually draw the character. */
        final float drawLetterLeft = -pLetter.mOffsetX;
        final float drawLetterTop = -pLetter.mOffsetY;

        this.drawLetter(characterAsString, drawLetterLeft, drawLetterTop);

        return bitmap;
    }

    protected void drawLetter(final String pCharacterAsString, final float pLeft, final float pTop) {
        this.mCanvas.drawText(pCharacterAsString, pLeft + Font.LETTER_TEXTURE_PADDING, pTop + Font.LETTER_TEXTURE_PADDING, this.mPaint);
    }

    public void prepareLetters(final char... pCharacters) throws FontException {
        for (final char character : pCharacters) {
            this.getLetter(character);
        }
    }

    private Letter createLetter(final char pCharacter) throws FontException {
        final String characterAsString = String.valueOf(pCharacter);

        final float textureWidth = this.mTextureWidth;
        final float textureHeight = this.mTextureHeight;

        this.updateTextBounds(characterAsString);
        final int letterLeft = this.mTextBounds.left;
        final int letterTop = this.mTextBounds.top;
        final int letterWidth = this.mTextBounds.width();
        final int letterHeight = this.mTextBounds.height();

        final Letter letter;

        final float advance = this.getLetterAdvance(characterAsString);

        final boolean whitespace = Character.isWhitespace(pCharacter) || (letterWidth == 0) || (letterHeight == 0);
        if (whitespace) {
            letter = new Letter(pCharacter, advance);
        } else {
            if ((this.mCurrentTextureX + Font.LETTER_TEXTURE_PADDING + letterWidth) >= textureWidth) {
                this.mCurrentTextureX = 0;
                this.mCurrentTextureY += this.mCurrentTextureYHeightMax + (2 * Font.LETTER_TEXTURE_PADDING);
                this.mCurrentTextureYHeightMax = 0;
            }

            if ((this.mCurrentTextureY + letterHeight) >= textureHeight) {
                throw new FontException("Not enough space for " + Letter.class.getSimpleName() + ": '" + pCharacter + "' on the " + this.mTexture.getClass().getSimpleName() + ". Existing Letters: " + SparseArrayUtils.toString(this.mManagedCharacterToLetterMap));
            }

            this.mCurrentTextureYHeightMax = Math.max(letterHeight, this.mCurrentTextureYHeightMax);

            this.mCurrentTextureX += Font.LETTER_TEXTURE_PADDING;

            final float u = this.mCurrentTextureX / textureWidth;
            final float v = this.mCurrentTextureY / textureHeight;
            final float u2 = (this.mCurrentTextureX + letterWidth) / textureWidth;
            final float v2 = (this.mCurrentTextureY + letterHeight) / textureHeight;

            letter = new Letter(pCharacter, this.mCurrentTextureX - Font.LETTER_TEXTURE_PADDING, this.mCurrentTextureY - Font.LETTER_TEXTURE_PADDING, letterWidth, letterHeight, letterLeft, letterTop, advance, u, v, u2, v2);
            this.mCurrentTextureX += letterWidth + Font.LETTER_TEXTURE_PADDING;
        }

        return letter;
    }

    protected void updateTextBounds(final String pCharacterAsString) {
        this.mPaint.getTextBounds(pCharacterAsString, 0, 1, this.mTextBounds);
    }

    public synchronized void update(final GLState pGLState) {
        if (this.mTexture.isLoadedToHardware()) {
            final ArrayList<Letter> lettersPendingToBeDrawnToTexture = this.mLettersPendingToBeDrawnToTexture;
            if (lettersPendingToBeDrawnToTexture.size() > 0) {
                this.mTexture.bind(pGLState);
                final PixelFormat pixelFormat = this.mTexture.getPixelFormat();

                final boolean preMultipyAlpha = this.mTexture.getTextureOptions().mPreMultiplyAlpha;
                for (int i = lettersPendingToBeDrawnToTexture.size() - 1; i >= 0; i--) {
                    final Letter letter = lettersPendingToBeDrawnToTexture.get(i);
                    if (!letter.isWhitespace()) {
                        final Bitmap bitmap = this.getLetterBitmap(letter);

                        final boolean useDefaultAlignment = MathUtils.isPowerOfTwo(bitmap.getWidth()) && MathUtils.isPowerOfTwo(bitmap.getHeight()) && (pixelFormat == PixelFormat.RGBA_8888);
                        if (!useDefaultAlignment) {
                            /* Adjust unpack alignment. */
                            GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
                        }

                        if (preMultipyAlpha) {
                            GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, letter.mTextureX, letter.mTextureY, bitmap);
                        } else {
                            pGLState.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, letter.mTextureX, letter.mTextureY, bitmap, pixelFormat);
                        }

                        if (!useDefaultAlignment) {
                            /* Restore default unpack alignment. */
                            GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, GLState.GL_UNPACK_ALIGNMENT_DEFAULT);
                        }

                        bitmap.recycle();
                    }
                }
                lettersPendingToBeDrawnToTexture.clear();

                System.gc();
            }
        }
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}